Skip to content

Conversation

FrancoisCapon
Copy link
Contributor

@FrancoisCapon FrancoisCapon commented Sep 1, 2025

Hello,

  • Usability: the Docker container user is the same as the Docker host, so there are no longer any file permission issues
$ id -u ; id -g
1000
1000
...
$ ls -ltrn
total 12
-rw-r--r-- 1 1000 1000   19 sept.  1 10:53 mkdocs.yml
drwxr-xr-x 2 1000 1000 4096 sept.  1 10:54 docs
drwxr-xr-x 5 1000 1000 4096 sept.  1 10:54 site
  • Security: by default, Docker publishes to 0.0.0.0, so the Docker host exposes the documentation to all other hosts

Important

Publishing container ports is insecure by default. Meaning, when you publish a container's ports it becomes available not only to the Docker host, but to the outside world as well.

If you include the localhost IP address (127.0.0.1, or ::1) with the publish flag, only the Docker host and its containers can access the published container port.

https://docs.docker.com/engine/network/#published-ports

$ ... -p 8000:8000 ...
$ ss -ltrn 
State           Recv-Q          Send-Q                    Local Address:Port                      Peer Address:Port          Process                                                        
LISTEN          0               4096                            0.0.0.0:8000                           0.0.0.0:*                                                     
LISTEN          0               4096                               [::]:8000                              [::]:* 
$ curl -I 192.168.1.30:8000
HTTP/1.0 200 OK
Date: Mon, 01 Sep 2025 09:46:27 GMT
Server: WSGIServer/0.2 CPython/3.11.13
Content-Type: text/html
Content-Length: 11125
$ curl -I localhost:8000 
HTTP/1.0 200 OK
Date: Mon, 01 Sep 2025 09:46:29 GMT
Server: WSGIServer/0.2 CPython/3.11.13
Content-Type: text/html
Content-Length: 11125

$ ... -p 127.0.0.1:8000:8000 ...
$ ss -ltrn 
State           Recv-Q          Send-Q                    Local Address:Port                      Peer Address:Port          Process                                                        
LISTEN          0               4096                            localhost:8000                           0.0.0.0:* 
$ curl -I 192.168.1.30:8000
curl: (7) Failed to connect to 192.168.1.30 port 8000 after 0 ms: Couldn't connect to server
$ curl -I localhost:8000 
HTTP/1.0 200 OK
Date: Mon, 01 Sep 2025 09:44:57 GMT
Server: WSGIServer/0.2 CPython/3.11.13
Content-Type: text/html
Content-Length: 11125                                               

@FrancoisCapon
Copy link
Contributor Author

I added the docker documentation reference of the ports publication on the initial comment.

@squidfunk
Copy link
Owner

Thanks for the PR! Could you please elaborate a little more why the changes are necessary?

Please keep in mind that we want to keep the getting started guide as simple as possible, and this does not include having a hardened setup. The Docker image is only intended for previewing, not for production. Additionally, please understand that we're currently very busy with bigger picture topics, so this change has to wait for a bit.

@FrancoisCapon
Copy link
Contributor Author

Thanks for your response.

  • By default the docker container user is root so the files writes belong to root and the docker host user can not handle it, that's here that my students stop using the docker image!
    drwxr-xr-x 2 root root 4096 sept.  9 10:57 docs
    -rw-r--r-- 1 root root   19 sept.  9 10:57 mkdocs.ym
    

The Docker image is only intended for previewing, not for production.

  • I know, but security by default is always the best policy. Your host can potentially exposed your "secret" documentation to other hosts (lan or bigger network) when all you need to do is add 127.0.0.1 to avoid that (docs.docker.com: Publishing container ports is insecure by default.)

  • The command lines can be monoline if you think it's simpler I can change it

    docker run --rm -u $(id -u):$(id -g) -v $(pwd):/docs squidfunk/mkdocs-material new .
    docker run --rm -u $(id -u):$(id -g) -v $(pwd):/docs -p 127.0.0.1:8000:8000 squidfunk/mkdocs-material
    docker run --rm -u $(id -u):$(id -g) -v $(pwd):/docs -p 127.0.0.1:8000:8000 squidfunk/mkdocs-material build
    

Additionally, please understand that we're currently very busy with bigger picture topics, so this change has to wait for a bit.

Yes, I understand.

Copy link
Owner

@squidfunk squidfunk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the PR and providing more information. I agree regarding security by default, and I wasn't aware that we can scope this to be less permissive. I thought exposing on 0.0.0.0 is the only thing that works. Does this also mean we can just use 127.0.0.1 in Docker, and map that through as well?

Please also read the comments, I have some minor remarks.

After you've [installed] Material for MkDocs, you can bootstrap your project
documentation using the `mkdocs` executable. Go to the directory where you want
your project to be located and enter:
After you've [installed] Material for MkDocs, you can bootstrap your documentation project then preview your documentation and finaly build your site using the `mkdocs` executable.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why this change is necessary? It doesn't have anything to do with what you described in your PR.

=== "Unix"

```
docker run --rm \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please format this as:

docker run --rm \
  ... (two spaces) \
  .... (and so on)

```
docker run --rm \
--user $(id -u):$(id -g) \
--volume $(pwd):/docs \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove -it?

--user $(id -u):$(id -g) \
--volume $(pwd):/docs \
squidfunk/mkdocs-material \
new . # bootstrap project
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the comment, this is not necessary, as it's described in the paragraph above.

--user $(id -u):$(id -g) \
--volume $(pwd):/docs \
--publish 127.0.0.1:8000:8000 \
squidfunk/mkdocs-material
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

docker run --rm \
--user $(id -u):$(id -g) \
--volume $(pwd):/docs \
squidfunk/mkdocs-material \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

@FrancoisCapon
Copy link
Contributor Author

FrancoisCapon commented Oct 7, 2025

I thought exposing on 0.0.0.0 is the only thing that works. Does this also mean we can just use 127.0.0.1 in Docker, and map that through as well?

  • When you use an IP you use the NIC "associated" with this IP
  • The 127.0.0.1 use a loopback NIC and can only communicate with himself (https://en.wikipedia.org/wiki/Localhost)
  • So if you bind a service to 127.0.0.1 in a container, the service can only be accessible into the container on its lo interface
$ docker run --entrypoint "/sbin/ip" squidfunk/mkdocs-material address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
38: eth0@if39: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
  • When you run a container using docker run without specifying a network, Docker automatically connects it to the default bridge network with a "container" NIC (example: eth0@if39 above) and docker set automatically a IP to that NIC.
  • The service must be bind to this IP, and because this IP is dynamic the service is binded to the special 0.0.0.0 IP (https://en.wikipedia.org/wiki/0.0.0.0#Binding) ; the service will be binded to lo (we don't care) and the container NIC (ex: eth0@if39).
$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED         STATUS         PORTS                      NAMES
4c265467c646   squidfunk/mkdocs-material   "/sbin/tini -- mkdoc…"   5 minutes ago   Up 5 minutes   127.0.0.1:8000->8000/tcp   relaxed_booth
$ docker exec -u 0 relaxed_booth apk add iproute2
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
...
OK: 40 MiB in 91 packages
$ docker exec relaxed_booth ss -ltp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess                       
LISTEN 0      5            0.0.0.0:8000      0.0.0.0:*    users:(("mkdocs",pid=7,fd=3))
  • Now, with for example --publish 127.0.0.1:1234:5678 all the outgoing TCP network traffic on port 1234 from the lo NIC host will be "routed" to the container NIC on port 5678 and vice versa for the response. The host think talking to localhost on port 1234 but in fact a talk to the container on port 5678!

Tip

So to put it on a nutshell:

  • CMD ["serve", "--dev-addr=0.0.0.0:8000"]
  • always bind the containerized service to 0.0.0.0 for technical reason
  • --publish 127.0.0.1:8000:8000
  • always map only your loopback host NIC to the container for security reason

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants